﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using Myotragus.Data.Util;
using NHibernate;
using NHibernate.Criterion;

namespace Myotragus.Data.Criteria
{
	public class CriteriaQuery<TEntity> : IPagedQuery<TEntity>,
		IOrderedQuery<TEntity>,
		IEquatable<CriteriaQuery<TEntity>>
	{
		private class OrderBySpec
		{
			public string Column { get; set; }

			public bool Descending { get; set; }
		}

		private readonly ICriteria _criteria;
		private readonly IList<OrderBySpec> _orderBy = new List<OrderBySpec>();

		public CriteriaQuery(ICriteria criteria)
		{
			_criteria = criteria;
		}

		public IPagedQuery<TEntity> Skip(int firstResult)
		{
			_criteria.SetFirstResult(firstResult);
			return this;
		}

		public IPagedQuery<TEntity> Take(int maxResults)
		{
			_criteria.SetMaxResults(maxResults);
			return this;
		}

		public IOrderedQuery<TEntity> OrderBy(string column)
		{
			_orderBy.Add(new OrderBySpec { Column = column });
			return this;
		}

		public IOrderedQuery<TEntity> OrderBy(Expression<Func<TEntity, object>> column)
		{
			return OrderBy(ReflectionHelper.ExtractMember(column).Name);
		}

		public IQuery<TEntity> SetOrderByExpression(IEnumerable<string> expressions)
		{
			var specs = CreateSpecs(expressions);
			SetOrderBySpecs(specs);

			return this;
		}

		public IQuery<TEntity> SetOrderByExpression(params string[] expressions)
		{
			return SetOrderByExpression((IEnumerable<string>)expressions);
		}

		private void SetOrderBySpecs(IEnumerable<OrderBySpec> specs)
		{
			_orderBy.Clear();

			foreach (var s in specs)
				_orderBy.Add(s);
		}

		private static IEnumerable<OrderBySpec> CreateSpecs(IEnumerable<string> expressions)
		{
			var specs = new List<OrderBySpec>();
			foreach (var exp in expressions)
				specs.AddRange(ExtractSpecs(exp));

			return specs;
		}

		private static readonly Regex OrderByExpressionParser = new Regex(
			@"(?<Column>(\w)+)(\s+(?<Direction>(asc|desc)))?",
			RegexOptions.Compiled | RegexOptions.IgnoreCase);

		private static IEnumerable<OrderBySpec> ExtractSpecs(string exp)
		{
			var match = OrderByExpressionParser.Match(exp);

			do
			{
				var column = match.Groups["Column"].Value;
				var direction = match.Groups["Direction"].Value;

				if (string.IsNullOrEmpty(direction))
					direction = "asc";

				yield return new OrderBySpec
					{
						Column = column,
						Descending = direction.Equals("desc",
							StringComparison.InvariantCultureIgnoreCase)
					};

				match = match.NextMatch();
			} while (match.Success);
		}

		public IDirectedOrderedQuery<TEntity> Asc
		{
			get
			{
				var index = _orderBy.Count - 1;
				_orderBy[index].Descending = false;

				return this;
			}
		}

		public IDirectedOrderedQuery<TEntity> Desc
		{
			get
			{
				var index = _orderBy.Count - 1;
				_orderBy[index].Descending = true;

				return this;
			}
		}

		public IOrderedQuery<TEntity> End()
		{
			return this;
		}

		public IEnumerable<TEntity> Execute()
		{
			SetOrders();
			return _criteria.List().Cast<TEntity>();
		}

		private void SetOrders()
		{
			_criteria.ClearOrders();

			foreach (var order in _orderBy)
				_criteria.AddOrder(order.Descending
					? Order.Desc(order.Column)
					: Order.Asc(order.Column));
		}

		public IOrderedQuery<TEntity> ThenBy(string column)
		{
			return OrderBy(column);
		}

		public IOrderedQuery<TEntity> ThenBy(Expression<Func<TEntity, object>> column)
		{
			return OrderBy(column);
		}

		public bool Equals(CriteriaQuery<TEntity> other)
		{
			if (ReferenceEquals(null, other))
				return false;

			return ReferenceEquals(this, other) || Equals(other._criteria, _criteria);
		}

		public override bool Equals(object obj)
		{
			if (ReferenceEquals(null, obj))
				return false;

			if (ReferenceEquals(this, obj))
				return true;

			return obj.GetType() == typeof(CriteriaQuery<TEntity>) && Equals((CriteriaQuery<TEntity>)obj);
		}

		public override int GetHashCode()
		{
			return _criteria.GetHashCode();
		}
	}
}